﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Runtime.Serialization;
using Framework;

namespace SchemaMonitor.json
{
    [DataContract]
    public class CFieldList : List<CField>
    {
        //Standard
        public CFieldList() : base() { }
        public CFieldList(int count) : base(count) { }
        public CFieldList(IEnumerable<CField> list) : base(list) { }
        static CFieldList() { ProtoBuf.Serializer.PrepareSerializer<CFieldList>(); }

        public readonly CMetadata Metadata;
        public bool HasMeta { get { return null != Metadata; } }

        public const string METADATA = "metadata";

        //Custom
        public CFieldList(Dictionary<string, object> dict, CNodeBasic parent, CUser u) : base(dict.Count)
        {
            var t = parent.Type;
            var m = GetMeta(t, dict, u);

            //update type signatures
            if (null != m)
            {
                var sigName = new CSignature(m.Type);
                var sigMeta = new CSignature(m);
                CTypeList.AddSignatures(t, sigName, sigMeta);
            }

            //Convert data for storage
            foreach (var pair in dict)
            {
                var f = CField.Factory(pair, parent, u);
                if (f != null)
                    this.Add(f);
            }
        }

        private static CMetadata GetMeta(CType t, Dictionary<string, object> dict, CUser u)
		{
			var id = CNodeRoot.GetIdAsString(dict);
			CMetadata m = null;

            //extract from data
            object meta = null;
            if (dict.TryGetValue(METADATA, out meta))
            {
                m = new CMetadata(meta, u, id);
                dict.Remove(METADATA);

                t.Metadata = m;
                t.TypeName = m.Type;
                t.Save();
            }

            //Use stored
            if (t.HasMeta)
                return t.Metadata;

            //Look up specially (just need an id)
            if (null != id)
                m = CEngine.DownloadMeta(id, u);
            if (null != m)
            {
                t.Metadata = m;
                t.Save();
            }
            return m;
        }

        internal CNodeBasic Parent
        {
            set
            {
                foreach (var i in this)
                    i.SetParent(value);
            }
        }


        public byte[] Serialise()
        {
            return CSerialise.Proto.Serialise_(this);
        }


        public string NameOrId
        {
            get
            {
                var n = string.Empty;
                if (Has("id"))
                    n = this["id"].AsString;

				if (Has("comment"))
					n = this["comment"].AsString;

				if (Has("message"))
					n = this["message"].AsString;

				var temp = n;
				if (Has("name"))
                    n = this["name"].AsString;
                if (Has("count"))
                {
                    var c = this["count"].AsLong;
                    if (c.HasValue && c.Value < int.MaxValue)
                        n = CUtilities.NameAndCount(n, (int)c.Value);
                }
				if (temp.Length > 0 && temp != n)
					n = string.Concat(n, ": ", CUtilities.Truncate(temp, 100));
				else
					n = CUtilities.Truncate(n, 100);
                return n;
            }
        }
		public new void Remove(CField f)
		{
			if (_indexByName.ContainsKey(f.Tag.TagName))
				_indexByName.Remove(f.Tag.TagName);
			base.Remove(f);
		}

        public bool Has(string name)
        {
            return IndexByName.ContainsKey(name);
        }
		public List<string> Tags { get { return IndexByName.Keys.ToList<string>(); } }
        public CField this[string name]
        {
            get
            {
                CField f = null;
                IndexByName.TryGetValue(name, out f);
                return f;
            }
        }
        [NonSerialized]
        private Dictionary<string, CField> _indexByName;
        private Dictionary<string, CField> IndexByName
        {
            get
            {
                if (null == _indexByName || _indexByName.Count != this.Count)
                {
                    var d = new Dictionary<string, CField>(this.Count);
                    var repeats = new CFieldList();
					foreach (var i in this)
					{
                        if (null != i.Tag)
                            try
                            {
                                d.Add(i.Tag.TagName, i);
                            }
                            catch
                            {
                                repeats.Add(i);
                            }
                        else
                            d.Add(Guid.NewGuid().ToString(), i);
					}
                    _indexByName = d;

                    foreach (var i in repeats)
                        this.Remove(i);
                }
                return _indexByName;
            }
        }

        public Dictionary<int, CField> ToDict()
        {
            var d = new Dictionary<int, CField>(this.Count);
            foreach (var i in this)
                d.Add(i.TagId, i);
            return d;
        }

		public CDiffList Merge(CFieldList updates, CUser u)
        {
            return Merge(updates, new List<int>(), u);
        }
        public CDiffList Merge(CFieldList updates, List<int> parentTagIds, CUser u)
        {
            var diff = new CDiffList();
			foreach (var i in updates)
			{
				var s = i.Tag.TagName;

                //New field
                if (!Has(s))
                {
                    this.Add(i);
                    diff.Add(new CDiff() { After = i, ParentTagIds = parentTagIds });
                    continue;
                }

                //Recursion
                var oldVal = this[s];
                if (oldVal is CFieldComposite && i is CFieldComposite)
                {
                    var tags = new List<int>(parentTagIds);
                    tags.Add(i.TagId);
                    try
                    {
                        CFieldComposite oldObj = (CFieldComposite)oldVal;
                        CFieldComposite newObj = (CFieldComposite)i;

                        //Replace if list size different
                        if (oldObj.List.Count != newObj.List.Count)
                        {
                            this.Remove(oldVal);
                            this.Add(i);
                            diff.Add(new CDiff() { Before=oldVal, After = i, ParentTagIds = parentTagIds });
                            continue;
                        }
                        //Merge if same
                        if (oldObj.List.Count == 1)
                        {
                            var oldItem = oldObj.AsComposite;
                            var newITem = newObj.AsComposite;
                            var subDiff = oldItem.Fields.Merge(newITem.Fields, tags, u);
                            if (subDiff.Count > 0)
                            {
                                diff.AddRange(subDiff);
                                continue;
                            }
                        }
                        else
                        {
                            var oldList = oldObj.AsCompositeList;
                            var newList = i.AsCompositeList;
                            for (var j = 0; j < oldList.Count; j++)
                            {
                                var subDiff = oldList[j].Fields.Merge(newList[j].Fields, tags, u);
                                if (subDiff.Count > 0)
                                {
                                    diff.AddRange(subDiff);
                                    continue;
                                }
                            }
                        }
                    }
                    catch
                    {
                        this.Remove(oldVal);
                        this.Add(i);
                        diff.Add(new CDiff() { Before = oldVal, After = i, ParentTagIds = parentTagIds });
                    }
                    continue;
                }


                if (oldVal is CFieldPointer && i is CFieldPointer)
                {
                    CFieldPointer oldObj = (CFieldPointer)oldVal;
                    if (i is CFieldPointer)
                    {
                        CFieldPointer newObj = (CFieldPointer)i;
                        bool same = true;
                        if (newObj.Refs.Count == oldObj.Refs.Count)
                            for (int j = 0; j < newObj.Refs.Count; j++)
                                if (newObj.Refs[j] != oldObj.Refs[j])
                                    same = false;
                        if (same)
                            continue;
                    }
                    else if (i is CFieldComposite)
                    {
                        var oldC = oldVal.AsPointer(u.All);
                        if (oldC.Root.Id == i.AsComposite.Fields["id"].AsString)
                            continue;
                    }
                    this.Remove(oldVal);
                    this.Add(i);
                    diff.Add(new CDiff() { Before = oldVal, After = i, ParentTagIds = parentTagIds });
                    continue;
                }

                if (i is CFieldPointer || i is CFieldComposite)
                {
                    this.Remove(oldVal);
                    this.Add(i);
                    diff.Add(new CDiff() { Before = oldVal, After = i, ParentTagIds = parentTagIds });
                    continue;
                }

                //No change
                if (i.AsString == oldVal.AsString)
                    continue;

                //Simple change
                try
                {
				    this[s].Value = i.Value;
				}
				catch
				{
					this.Remove(oldVal);
					this.Add(i);
                    diff.Add(new CDiff() { Before = oldVal, After = i, ParentTagIds = parentTagIds });
                }
			}
            return diff;
		}
    }
}
